Skip to content

migrate Joi schemas and validation utils to Zod#465

Closed
CKodidela wants to merge 3 commits intocameri:mainfrom
CKodidela:migrate-joi-to-zod
Closed

migrate Joi schemas and validation utils to Zod#465
CKodidela wants to merge 3 commits intocameri:mainfrom
CKodidela:migrate-joi-to-zod

Conversation

@CKodidela
Copy link
Copy Markdown
Collaborator

@CKodidela CKodidela commented Apr 12, 2026

Description

Replaces the joi validation library with zod across all schemas and validation utilities.

Changed files:

  • src/schemas/base-schema.ts hex/lowercase via regex, integer checks via .int(), custom createdAt via
    .refine(), tagSchema using z.tuple().rest()
  • src/schemas/event-schema.ts z.object().strict() replaces .unknown(false)
  • src/schemas/filter-schema.ts .catchall() + .superRefine() handle dynamic #[a-z] tag filter keys
  • src/schemas/message-schema.ts z.union() replaces alternatives().conditional(); z.tuple().rest() with
    superRefine for REQ bounds
  • src/utils/validation.ts validateSchema/attemptValidation rewritten against Zod API
  • src/adapters/web-socket-adapter.ts error name check updated from ValidationError to ZodError

Related Issue

Closes #455

Motivation and Context

joi is a large runtime dependency. zod provides equivalent validation with a TypeScript-first design, better type
inference, and a smaller footprint. All existing validation behaviour is preserved.

How Has This Been Tested?

All existing unit tests pass (381 passing) after updating schema spec files to work with Zod's error format. The 2
pre-existing failures in settings.spec.ts are unrelated Windows path separator issues.

Types of changes

  • Non-functional change (docs, style, minor refactor)

Checklist:

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my code changes.
  • All new and existing tests passed.

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@cameri @phoenix-server this PR is up for review

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@copilot can you resolve the merge conflicts

@cameri
Copy link
Copy Markdown
Owner

cameri commented Apr 18, 2026

@CKodidela I dont think you can trigger. either way doesnt seem to be working

@CKodidela
Copy link
Copy Markdown
Collaborator Author

No worries Cameri, i will resolve them on my own

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@cameri resolved the merge conflicts, can run checks

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the project’s runtime validation from Joi to Zod, updating core NIP-01 schemas and the shared validation utilities so the rest of the codebase can validate/parse inputs via Zod.

Changes:

  • Replaced Joi schemas with Zod equivalents for base/event/filter/message schemas (including strict object handling and custom refinements).
  • Rewrote validateSchema / attemptValidation utilities to use Zod parsing.
  • Updated WebSocket adapter and unit tests to align with Zod error behavior; swapped dependencies from joi to zod.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
test/unit/utils/validation.spec.ts Updates unit tests for validation helpers to expect ZodError.
test/unit/schemas/message-schema.spec.ts Adjusts message schema tests for Zod-driven validation results.
test/unit/schemas/filter-schema.spec.ts Adjusts filter schema tests for Zod-driven validation results.
test/unit/schemas/event-schema.spec.ts Adjusts event schema tests for Zod-driven validation results.
src/utils/validation.ts Replaces Joi-based validation helpers with Zod-based parsing helpers.
src/schemas/message-schema.ts Migrates message tuple/union validation from Joi conditionals to Zod tuples/unions.
src/schemas/filter-schema.ts Migrates filter schema to Zod with catchall + superRefine for dynamic #[a-z] keys.
src/schemas/event-schema.ts Migrates event schema to z.object(...).strict() to reject unknown keys.
src/schemas/base-schema.ts Migrates core primitives (hex, ints, timestamps, tags) from Joi to Zod.
src/adapters/web-socket-adapter.ts Updates malformed-message handling to recognize Zod validation failures.
package.json Removes Joi and adds Zod dependency.
package-lock.json Lockfile updates reflecting Joi removal and Zod addition.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 103 to 108
it('returns error if filter is missing', () => {
(message as any[]).splice(2, 2)

const result = validateSchema(messageSchema)(message)
expect(result).to.have.nested.property('error.message', '"REQ message" does not contain [filter]')
expect(result).to.have.property('error').that.is.not.undefined
})
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assertions were loosened to only check that an error exists, which no longer verifies which validation rule failed (or that the failure is on the expected path). To keep the tests meaningful with Zod, consider asserting against result.error.issues (e.g., expected path/code) rather than dropping specificity entirely.

Copilot uses AI. Check for mistakes.
Comment on lines 99 to 103
cases[prop].forEach(({ transform, message }) => {
it(`${prop} ${message}`, () => expect(
validateSchema(filterSchema)(transform(filter))
).to.have.nested.property('error.message', `"${prop}" ${message}`))
).to.have.property('error').that.is.not.undefined)
})
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test cases loop no longer asserts the expected failure reason for each prop case; it only checks that some error occurred. Consider asserting on error.issues (path/code) for each case so regressions (wrong key failing, wrong rule failing) don't slip through.

Copilot uses AI. Check for mistakes.
Comment on lines 61 to 65
it('returns error if unknown key is provided', () => {
Object.assign(event, { unknown: 1 })

expect(validateSchema(eventSchema)(event)).to.have.nested.property('error.message', '"unknown" is not allowed')
expect(validateSchema(eventSchema)(event)).to.have.property('error').that.is.not.undefined
})
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These checks were changed from validating the specific error message to only checking that error is present. That makes the test less able to catch schema regressions. Consider asserting on error.issues (e.g., an issue with path: ['unknown'] for unknown-key cases) instead of dropping the assertion detail entirely.

Copilot uses AI. Check for mistakes.
Comment thread src/utils/validation.ts Outdated
Comment on lines +4 to +8
try {
return { value: schema.parse(input), error: undefined }
} catch (error) {
return { value: undefined, error: error as z.ZodError }
}
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateSchema currently catches all thrown errors from schema.parse and returns them cast as ZodError. This can silently convert unexpected runtime errors (e.g., thrown inside a .refine() callback) into a validation result and potentially cause callers to treat real bugs as user input errors. Prefer schema.safeParse(input) (which guarantees a ZodError on failure), or only handle error instanceof z.ZodError and rethrow anything else.

Suggested change
try {
return { value: schema.parse(input), error: undefined }
} catch (error) {
return { value: undefined, error: error as z.ZodError }
}
const result = schema.safeParse(input)
if (result.success) {
return { value: result.data, error: undefined }
}
return { value: undefined, error: result.error }

Copilot uses AI. Check for mistakes.
Comment thread src/adapters/web-socket-adapter.ts Outdated
Comment on lines 182 to 184
} else if (error.name === 'SyntaxError' || error.name === 'ZodError') {
debug('invalid message client %s (%s): %s', this.clientId, this.getClientAddress(), error.message)
this.sendMessage(createNoticeMessage(`invalid: ${error.message}`))
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adapter is branching on error.name === 'ZodError'. This is brittle (name can be changed and any error can spoof it). Prefer importing ZodError and checking error instanceof ZodError so only genuine validation failures are handled as malformed client messages.

Copilot uses AI. Check for mistakes.
Comment on lines +183 to 185
debug('invalid message client %s (%s): %s', this.clientId, this.getClientAddress(), error.message)
this.sendMessage(createNoticeMessage(`invalid: ${error.message}`))
} else {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createNoticeMessage(invalid: ${error.message}) will send the raw ZodError.message to clients, which is typically a stringified list of issues and can be verbose and hard to read. Consider formatting this down to a stable, single-line summary (e.g., first issue message + path) before sending, to keep notices small and client-friendly.

Copilot uses AI. Check for mistakes.
@cameri
Copy link
Copy Markdown
Owner

cameri commented Apr 18, 2026

@CKodidela Ci checks failing now, could you please address?

@CKodidela
Copy link
Copy Markdown
Collaborator Author

Sure will do it.

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@cameri I believe the first three suggestions from copilot arent worth changing, these tests were loosened during the Joi to Zod migration, the tests are not broken they're just less specific.

I will fix the rest and run checks locally

@CKodidela
Copy link
Copy Markdown
Collaborator Author

@cameri resolved the suggestions, ran checks locally all checks passed.
can run checks

@cameri
Copy link
Copy Markdown
Owner

cameri commented Apr 18, 2026

@CKodidela ci checks are failing

@CKodidela
Copy link
Copy Markdown
Collaborator Author

Hmm i guess this takes time let me see the log, checks are passing locally should find out the cause

@CKodidela
Copy link
Copy Markdown
Collaborator Author

Got it i didnt make a conventional commit at the beginning, let me close this pr and make a new one

@CKodidela CKodidela closed this Apr 18, 2026
@CKodidela CKodidela deleted the migrate-joi-to-zod branch April 18, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrate Joi schemas and associated types to Zod

3 participants